import queue
import shutil, importlib
import threading, signal, time, sys, logging, platform
from typing import Any, Optional
from xml.etree import ElementTree as ET
from feasytools import ArgChecker, time2str
from pathlib import Path
from fplugins import *
from fstatistics import *
from ftraffic import *

HELP_STRING = f"""EV负荷模拟器 - 使用帮助
{sys.argv[0]} -d <配置文件夹> [-b <开始时间>] [-e <结束时间>] [-l <数据记录步长>] [-o <输出文件夹>] [-f <输出文件类型>]\\
[-vc <车辆数>] [-vs <随机种子>] [-log <要记录的数据>] [--no-plg <要禁用的插件>] [--show] [--no-daemon] [--copy] [--debug] [-h/help]
    h/help: 显示本帮助信息
    d: 配置文件夹, 默认值12nodes
    b: 开始时间, 单位为秒, 默认从SUMO配置中读取
    e: 结束时间, 单位为秒, 默认从SUMO配置中读取
    l: 数据记录步长, 单位为秒, 默认值10
    o: 输出文件夹, 默认为当前目录下的results文件夹
    f: 输出文件类型, 默认值.sdt 可选项为.sdt(速度最快),.sdt.gz(文件最小)和.csv(可读性最好)
    vc: 当没有行程文件时, 自动生成的车辆数, 默认值3000
    seed: 自动生成车辆以及SUMO使用的随机种子, 默认值为当前时间(ns)%65536
    log: 要记录的数据, 包括电动汽车(ev), 充电站(cs)和发电机(gen), 多项数据用逗号隔开, 默认值为"cs,gen"
    show: 启用该选项以GUI模式启动. 该选项仅在Linux下有用, 在Windows下请调整EVisConf/__init__.py的WINDOWS_VISSUALIZE来改变可见性级别
    no-daemon: 启用该选项以将仿真线程与显示窗口分离, 不启用时显示窗口一旦关闭, 仿真也会停止
    no-plg: 禁用指定的插件, 多个插件用逗号隔开, 默认不禁用任何插件
    copy: 仿真结束后将配置文件复制到输出文件夹, 默认不复制
    debug: 针对图形仿真启用调试模式, 图形仿真出错时会输出详细的错误信息
"""


def error_exit(err=None, print_help: bool = False):
    if err:
        print(f"Error: {err}")
    print()
    if print_help:
        print(HELP_STRING)
    sys.exit()


def get_sim_config(file: str):
    root = ET.ElementTree(file=file).getroot()
    tnode = root.find("time")
    inode = root.find("input")
    assert isinstance(tnode, ET.Element) and isinstance(inode, ET.Element)
    bnode = tnode.find("begin")
    enode = tnode.find("end")
    nfnode = inode.find("net-file")
    assert (
        isinstance(bnode, ET.Element)
        and isinstance(enode, ET.Element)
        and isinstance(nfnode, ET.Element)
    )
    return (
        int(bnode.attrib.get("value", "-1")),
        int(enode.attrib.get("value", "-1")),
        nfnode.attrib.get("value"),
    )


if __name__ == "__main__":
    try:
        args = ArgChecker()
    except ValueError as e:
        error_exit(e, True)
    if args.pop_bool("h") or args.pop_bool("help"):
        error_exit(None, True)

    if platform.system() == "Windows":
        if "show" in args.keys():
            print(
                "警告: show选项在Windows下没有意义, 请在ftraffic/params.py中调整WINDOWS_VISSUALIZE来改变可见性级别"
            )
            args.pop_bool("show")
        visible = WINDOWS_VISSUALIZE
    else:
        visible = args.pop_bool("show")

    # 加载参数
    kwargs = {
        "cfg_dir": args.pop_str("d"),  # 12nodes or 37nodes
        "traffic_step": args.pop_int("l", 10),
        "start_time": args.pop_int("b", -1),
        "end_time": args.pop_int("e", -1),
        "outdir": args.pop_str("o", ".\\results\\"),
        "result_fmt": args.pop_str("f", ".sdt"),
        "no_plg": args.pop_str("no-plg", ""),
        "log": args.pop_str("log", "cspk,gen"),
        "def_veh_cnt": args.pop_int("vc", 3000),
        "seed": args.pop_int("seed", time.time_ns() % 65536),
        "copy": args.pop_bool("copy"),
    }
    no_deamon = args.pop_bool("no-daemon")
    debug_mode = args.pop_bool("debug")
    if debug_mode and not visible:
        print("警告: debug选项在命令行模式下没有意义, 将自动关闭")
        debug_mode = False
    if len(args.values()) > 0:
        for key in args.keys():
            error_exit(f"存在非法参数'{key}'", True)

StopSignal = False


def simulate(
    *,
    cfg_dir: str,
    traffic_step: int,
    start_time: int,
    end_time: int,
    outdir: str,
    result_fmt: TableFileSuffix,
    no_plg: str,
    log: str,
    seed: int,
    def_veh_cnt: int,
    copy: bool,
    vb=None,
    mpQ: Optional[queue.Queue[tuple[int, str]]] = None,
    clntID: int = -1,
) -> tuple[TrafficInst, StaWriter]:

    assert result_fmt in TableFileSuffixList

    def elprint(con: str, Qcon: str = "", *, file: Any = sys.stdout, end="\n"):
        if mpQ:
            try:
                if Qcon != "":
                    mpQ.put_nowait((clntID, Qcon))
            except:
                print("警告: 与主进程通信失败, 仿真将继续进行")
        else:
            print(con, file=file, end=end)

    # 检查是否存在上次的results
    pres = Path(outdir)
    if pres.is_dir() and (pres / "time_line.pkl.gz").exists():
        tm = time.strftime("%Y%m%d_%H%M%S", time.localtime(pres.stat().st_mtime))
        pres.rename(f".\\results_{tm}_at{time.time_ns()}")
    pres.mkdir(parents=True, exist_ok=True)

    # 创建cproc.log
    ostream = open(str(pres / "cproc.log"), "w", encoding="utf-8")
    global StopSignal
    StopSignal = False

    def eh(signum, frame):
        global StopSignal
        elprint("收到Ctrl-C退出信号, 提前退出", "exit")
        StopSignal = True

    proj_dir = Path(cfg_dir)
    proj_name = proj_dir.name

    # 探测SUMO配置
    sumocfg_file = proj_dir / (proj_name + ".sumocfg")
    if sumocfg_file.exists():
        sumocfg_file = str(sumocfg_file)
        elprint(f"  SUMO: {sumocfg_file}")
    else:
        raise FileNotFoundError("未找到SUMO配置文件")
    _stt, _edt, _rnet = get_sim_config(sumocfg_file)

    # 探测路网文件
    rnet_file = proj_dir / (proj_name + ".net.xml")
    if rnet_file.exists():
        rnet_file = str(rnet_file)
        elprint(f"  路网: {rnet_file}")
    else:
        if _rnet is None:
            raise RuntimeError("从未指定路网文件, 请检查SUMO配置文件")
        rnet_file = str(proj_dir / _rnet)
        elprint(f"  路网: {rnet_file}")
    if rnet_file is None:
        raise FileNotFoundError("未找到路网文件")

    # 检查车辆与行程
    veh_file = proj_dir / (proj_name + ".veh.xml")
    if veh_file.exists():
        vehicles = str(veh_file)
        elprint(f"  行程: {vehicles}")
    elif def_veh_cnt is not None:
        elprint(f"  行程: 自动生成{def_veh_cnt}辆EV")
        vehicles = EVsGenerator("出行链概率表", cfg_dir, seed).genEVs(
            int(def_veh_cnt), 1, str(veh_file)
        )
    else:
        raise ValueError("未找到行程文件, 也未指定生成车辆数目")

    # 检查充电站文件
    cs_file = proj_dir / (proj_name + ".cs.xml")
    if not cs_file.exists():
        from cspk_gen import generate_cspk

        print("  未找到快充站文件, 将自动生成快充站文件")
        generate_cspk(
            seed,
            str(proj_dir),
            0,
            "",
            DEFAULT_SELL_PRICE[1][0],
            DEFAULT_CS_SLOTS,
            DEFAULT_PK_SLOTS,
            non_pk=True,
            silent=True,
        )
    cs_list = str(cs_file)
    elprint(f"  快充: {cs_file}")

    pk_file = proj_dir / (proj_name + ".pk.xml")
    if not pk_file.exists():
        from cspk_gen import generate_cspk

        print("  未找到慢充站文件, 将自动生成慢充站文件")
        generate_cspk(
            seed,
            str(proj_dir),
            0,
            "",
            DEFAULT_SELL_PRICE[1][0],
            DEFAULT_CS_SLOTS,
            DEFAULT_PK_SLOTS,
            non_cs=True,
            silent=True,
        )
    pk_list = str(pk_file) if pk_file.exists() else None
    elprint(f"  慢充: {'自动生成' if pk_list is None else pk_file}")

    # 检查起止时间
    if start_time == -1:
        start_time = _stt
    if end_time == -1:
        end_time = _edt
    if start_time == -1 or end_time == -1:
        raise ValueError("仿真起止时间未指定")
    sim_duration = end_time - start_time
    elprint(f"  起止时间: {start_time} ~ {end_time}")

    # 创建仿真实例
    myNet = TrafficInst(
        rnet_file, vehicles, start_time, end_time, seed, cs_list, pk_list, ostream
    )

    # 加载外部插件
    PluginMan.ResetPlugins() # 这对于Parasim有用
    explugdir = Path(os.getcwd()) / "external_plugins"
    if explugdir.exists():
        for plugfile in explugdir.iterdir():
            if plugfile.is_file() and plugfile.suffix == ".py":
                plugname = plugfile.stem
                try:
                    module = importlib.import_module(f"external_plugins.{plugname}")
                    PluginMan.Register(plugname, module.exports[0], module.exports[1])
                except Exception as e:
                    elprint(f"  未能加载插件: {plugname}, {e}")

    # 检查插件
    plg_file = proj_dir / (proj_name + ".plg.xml")
    PluginMan.LoadFromFile(
        str(plg_file), myNet, list(map(lambda x: x.strip().lower(), no_plg.split(",")))
    )
    for plugname, plugin in PluginMan.GetPlugins().items():
        elprint(f"  插件: {plugname} - {plugin.Description}")

    # 创建数据记录器
    log_item = log.strip().lower().split(",")
    mySta = StaWriter(str(pres), result_fmt, myNet, PluginMan.GetPlugins())
    for itm in log_item:
        mySta.Add(itm)

    # 开始仿真
    if vb is None and clntID != -1:
        signal.signal(signal.SIGINT, eh)
    st_time = time.time()
    last_print_time = 0
    myNet.simulation_start(sumocfg_file, rnet_file, start_time, vb is not None)

    while myNet.current_time < end_time:
        PluginMan.PreStepAll(myNet.current_time)
        myNet.simulation_step(traffic_step)
        PluginMan.PostStepAll(myNet.current_time)
        mySta.Log(myNet.current_time)

        # 可视化
        if vb is not None:
            counter = [0, 0, 0, 0, 0]
            for veh in myNet.vehicles.values():
                counter[veh.status] += 1
            upd = {
                "Driving": counter[VehStatus.driving],
                "Pending": counter[VehStatus.pending],
                "Charging": counter[VehStatus.charging],
                "Parking": counter[VehStatus.parking],
                "Depleted": counter[VehStatus.depleted],
            }
            upd.update(zip(myNet._cs.get_CS_names(), myNet._cs.get_veh_count()))
            vb.set_val(upd)
        else:
            ctime = time.time()
            if ctime - last_print_time > 1:
                progress = 100 * (myNet.current_time - start_time) / sim_duration
                eta = (
                    time2str((ctime - st_time) * (100 - progress) / progress)
                    if ctime - st_time > 3
                    else "N/A"
                )
                elprint(
                    f"\r进度: {progress:.2f}%, {myNet.current_time}/{end_time}, Elapsed: {time2str(ctime-st_time)}, ETA: {eta} ",
                    str(progress),
                    end="",
                )
                last_print_time = ctime
        if StopSignal:
            break
    dur = time.time() - st_time
    ostream.close()
    myNet.simulation_stop()
    mySta.close()
    if copy:
        # shutil.copy(sumocfg_file,pres/(proj_name+".sumocfg"))
        # shutil.copy(rnet_file,pres/(proj_name+".net.xml"))
        shutil.copy(veh_file, pres / ("veh.xml"))
        shutil.copy(cs_file, pres / ("cs.xml"))
        shutil.copy(pk_file, pres / ("pk.xml"))
        shutil.copy(plg_file, pres / ("plg.xml"))
    elprint(
        f"\r仿真{'中断' if StopSignal else '完成'}. 用时: {time2str(dur)}                                      ",
        "exit",
    )
    return myNet, mySta


if __name__ == "__main__":
    if visible:
        try:
            from fgui import ViewBox
        except:
            print("警告: 未找到GUI模块, 请检查是否安装了tkinter库, 将切换到命令行模式")
            visible = False

    if visible:
        vb = ViewBox(
            ["Driving", "Pending", "Charging", "Parking", "Depleted"],
            "Simulator Dashboard",
        )
        kwargs["vb"] = vb

        def work():
            try:
                simulate(**kwargs)
            except Exception as e:
                if debug_mode:
                    raise e
                logging.exception(e)
            vb.close()

        th = threading.Thread(target=work, daemon=not no_deamon).start()
        vb.mainloop()
    else:
        print("仿真开始, 按Ctrl-C中断")
        simulate(**kwargs)
